﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Cache;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Web;
using SyncInfoDict = System.Collections.Generic.Dictionary<string, HttpPublish.SyncInfo>;

namespace HttpPublish
{
    public class SyncInfo
    {
        public bool Download;
        public DateTime LDate;
        public UInt64 LSize;
        public string Name;
        public DateTime RDate;
        public UInt64 RSize;
        public int SizeDelta;
        public bool Transfer;
        public bool Transfered;
        public bool Upload;
        public string Msg;
    }

    public class Publisher
    {
        private const int Pool = 10;
        private const string PublishTemplate = "path={0}&file={1}&password={2}&createFile=true&createPath=true&content={3}";
        private static string _remoteUri;
        private static Publisher _singletonPublisher;
        private PublishConfigration _config;
        private long _errors;
        private string _localPath;

        private long _sent;
        private long _skipped;
        private long _total;
        private SyncInfo[] _trackedFiles;
        private int _used;

        public Publisher(PublishConfigration config, IDictionary<string, TrackFileInfo> lFiles, IDictionary<string, TrackFileInfo> rFiles)
        {
            _singletonPublisher = this;
            SetConfig(config);
            _trackedFiles = ResolveFiles(config, lFiles, rFiles);
        }

        private Publisher(PublishConfigration config)
        {
            _singletonPublisher = this;
            SetConfig(config);
        }

        private void SetConfig(PublishConfigration config)
        {
            _localPath = config.Local;
            _remoteUri = Path.Combine(config.Host, "pub.put.php");
            _config = config;
        }

        public static SyncInfo[] GetSyncFiles(PublishConfigration config)
        {
            var remoteTracker = new RemoteTracker(config);
            Dictionary<string, TrackFileInfo> remoteFiles = remoteTracker.GetFiles();

            var localTracker = new LocalTracker(config);
            Dictionary<string, TrackFileInfo> localFiles = localTracker.GetFiles();

            return ResolveFiles(config, localFiles, remoteFiles);
        }

        public static SyncInfo[] ResolveFiles(PublishConfigration config, IDictionary<string, TrackFileInfo> lFiles, IDictionary<string, TrackFileInfo> rFiles)
        {
            //var trackedFiles = new SyncInfo[lFiles.Count];
            var trackedFiles = new Dictionary<string, SyncInfo>();

            // Find local files that need uploading
            foreach (string name in lFiles.Keys)
            {
                var item = new SyncInfo();
                item.Name = lFiles[name].Name;
                if (rFiles.ContainsKey(name))
                {
                    item.LSize = lFiles[name].Size;
                    item.LDate = LocalTracker.UtcToDateTime(lFiles[name].Time);
                    item.RDate = LocalTracker.UtcToDateTime(rFiles[name].Time);
                    item.SizeDelta = (int) (lFiles[name].Size - rFiles[name].Size);
                    if (item.LDate > item.RDate)
                        item.Upload = item.Transfer = true;
                    if (item.SizeDelta != 0)
                        item.Upload = item.Transfer = true;
                }
                else
                {
                    item.LDate = LocalTracker.UtcToDateTime(lFiles[name].Time);
                    item.Upload = item.Transfer = true;
                }
                trackedFiles.Add(item.Name, item);
            }

            // Find local files that need downloading
            foreach (string name in rFiles.Keys)
            {
                SyncInfo item;

                // Don't download files that are being uploaded
                if (trackedFiles.ContainsKey(name))
                {
                    item = trackedFiles[name];
                    if (item.Upload)
                        continue;
                }
                else
                {
                    item = new SyncInfo();
                    trackedFiles.Add(name, item);
                }
                item.Name = rFiles[name].Name;
                if (lFiles.ContainsKey(name))
                {
                    item.RSize = rFiles[name].Size;
                    item.LDate = LocalTracker.UtcToDateTime(lFiles[name].Time);
                    item.RDate = LocalTracker.UtcToDateTime(rFiles[name].Time);
                    item.SizeDelta = (int) (lFiles[name].Size - rFiles[name].Size);
                    if (item.LDate < item.RDate)
                        item.Download = item.Transfer = true;
                    if (item.SizeDelta != 0)
                        item.Download = item.Transfer = true;
                }
                else
                {
                    item.RDate = LocalTracker.UtcToDateTime(rFiles[name].Time);
                    item.Download = item.Transfer = true;
                }
            }

            var retArray = new SyncInfo[trackedFiles.Count];
            trackedFiles.Values.CopyTo(retArray, 0);
            return retArray;
        }

        public static void Publish(PublishConfigration config, SyncInfoDict infoDict)
        {
            if (_singletonPublisher == null)
                new Publisher(config);
            Publisher instance = _singletonPublisher;
            Debug.Assert(instance != null, "instance != null");
            instance._trackedFiles = new SyncInfo[infoDict.Count];

            int cnt = 0;

            foreach (SyncInfo info in infoDict.Values)
            {
                instance._trackedFiles[cnt++] = info;
            }
            instance.Publish();
        }

        public void Publish()
        {
            _used = Pool;
            _sent = _skipped = _errors = _total = 0;

            for (int i = 0; i < _trackedFiles.Length; i++)
            {
                int infoIndex = i;
                SyncInfo info = _trackedFiles[infoIndex];

                _total++;

                if (info.Transfer == false)
                {
                    Write(info, "-----<>");
                    _skipped++;
                    continue;
                }

                if (info.Upload == false)
                    continue;

                if (_used > 0)
                {
                    _used--;
                    new Thread(() => HttpPut(_remoteUri, infoIndex)).Start();
                    //new Thread(() => HttpPutMulti(_remoteUri, infoIndex)).Start();
                    Write(info, "Sending");
                }
                else
                {
                    i--;
                    Thread.Sleep(49);
                }
            }

            while (_used < Pool)
            {
                Thread.Sleep(51);
            }

            Log.Trace("   Sent {0}   Skipped {1}   Errors {2}   Total {3}", _sent, _skipped, _errors, _total);
        }

        private static void Write(SyncInfo info, string label)
        {
            string msg = String.Format("{0,8} {1} lcl {2} rmt {3} szchg {4}",
                label,
                info.Name.PadRight(65, '.'),
                info.LDate.ToLocalTime().ToString(CultureInfo.InvariantCulture).PadRight(23, '.'),
                info.RDate.ToLocalTime().ToString(CultureInfo.InvariantCulture).PadRight(23, '.'), info.SizeDelta
                );
            if (!String.IsNullOrEmpty(info.Msg))
                msg += "\r\n" + info.Msg;
            Log.Trace(msg);
        }

        public static void SyncDates(PublishConfigration config, SyncInfoDict infoDict)
        {
            foreach (var pair in infoDict)
            {
                if (pair.Value.Transfer && pair.Value.Download)
                {
                    SyncDate(config, pair.Value.Name, pair.Value.RDate);
                }
            }
        }

        private static void SyncDate(PublishConfigration config, string fileName, DateTime date)
        {
            try
            {
                fileName = Path.Combine(config.Local, fileName);
                fileName = fileName.Replace(@"/", @"\");
                if (File.Exists(fileName))
                    File.SetLastWriteTime(fileName, date);
            }
            catch (Exception ex)
            {
                Log.Trace(ex.ToString());
            }
        }

        private void HttpPut(string uri, int i)
        {
            try
            {
                SyncInfo info = _trackedFiles[i];
                var webRequest = (HttpWebRequest) WebRequest.Create(uri);
                webRequest.Method = "POST";
                webRequest.Timeout = 60000;
                webRequest.AllowAutoRedirect = false;
                webRequest.Credentials = new NetworkCredential(_config.User, _config.Password);
                webRequest.PreAuthenticate = true;
                webRequest.CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore);

                byte[] data = File.ReadAllBytes(Path.Combine(_localPath, info.Name));

                string content = String.Format(PublishTemplate, HttpUtility.UrlEncode(_config.Remote), HttpUtility.UrlEncode(info.Name), HttpUtility.UrlEncode(_config.Password), HttpUtility.UrlEncode(data));

                webRequest.ContentLength = content.Length;
                webRequest.ContentType = "application/x-www-form-urlencoded";

                try
                {
                    Stream dataStream = webRequest.GetRequestStream();
                    if (content.Length > 0)
                    {
                        byte[] contentBytes = (new ASCIIEncoding()).GetBytes(content);
                        dataStream.Write(contentBytes, 0, contentBytes.Length);
                    }
                    dataStream.Close();
                }
                catch (Exception ex)
                {
                    _errors++;
                    info.Msg = ex.ToString();
                    Write(info, "Error");
                    return;
                }

                WebResponse response;
                Stream responseStream;
                try
                {
                    response = webRequest.GetResponse();
                    responseStream = response.GetResponseStream();
                }
                catch (Exception ex)
                {
                    _errors++;
                    info.Msg = ex.ToString();
                    Write(info, "Error");
                    return;
                }

                if (responseStream != null)
                {
                    var reader = new StreamReader(responseStream, Encoding.UTF8);

                    ResultSet remoteResultSet;
                    try
                    {
                        remoteResultSet = JsonHelper.Deserialize<ResultSet>(reader.ReadToEnd());
                    }
                    catch (Exception)
                    {
                        remoteResultSet = new ResultSet {Result = false, Msg = "bad response from server"};
                    }

                    if (remoteResultSet.Result)
                    {
                        info.RDate = LocalTracker.UtcToDateTime(remoteResultSet.LastChanged*1000);
                        info.SizeDelta = (int) (info.LSize - remoteResultSet.FileSize);
                        info.Transfered = true;
                        _sent++;
                        Write(info, "Sent");
                        SyncDate(_config, info.Name, info.RDate);
                    }
                    else
                    {
                        _errors++;
                        Log.Trace("Error sendinging file {0}", info.Name);
                        if (!String.IsNullOrEmpty(remoteResultSet.Msg))
                            Log.Trace("Message: {0}", remoteResultSet.Msg);
                        if (!String.IsNullOrEmpty(remoteResultSet.AltMsg))
                            Log.Trace("Message: {0}", remoteResultSet.AltMsg);
                        if (!String.IsNullOrEmpty(remoteResultSet.Error))
                            Log.Trace("Error: {0}", remoteResultSet.Error);
                    }
                }

                response.Close();
            }
            finally
            {
                _used++;
            }
        }

        private void HttpPutMulti(string uri, int i)
        {
            try
            {
                SyncInfo info = _trackedFiles[i];
                var dSend = new Dictionary<string, string>();
                dSend[info.Name] = "file://" + Path.Combine(_localPath, info.Name);

                MultipartUpload.Post(uri, null, dSend, _config);
            }
            finally
            {
                _used++;
            }
        }
        public static string ScriptPath(string fileName)
        {
            string filePath = Path.Combine(Path.GetDirectoryName(Assembly.GetCallingAssembly().Location), fileName);
            return filePath;
        }
    }
}